3-3 第一个nestjs应用: Hello World
项目目录结构扩展说明
核心目录深度解析
src/ 目录详解
- 主要文件结构:
src/ ├── app.controller.ts # 控制器:处理HTTP请求 ├── app.module.ts # 根模块:组织应用结构 ├── app.service.ts # 服务:核心业务逻辑 └── main.ts # 应用入口:启动NestJS应用
text - 扩展功能:
- 可创建子模块目录(如
users/
)实现功能模块化 - 支持DTO(数据传输对象)目录存放接口定义
- 可创建子模块目录(如
node_modules/ 目录注意事项
- 最佳实践:
- 使用
.npmrc
配置镜像源加速安装 - 通过
npm ci
命令确保依赖版本一致性 - 建议添加到
.gitignore
避免提交到版本库
- 使用
test/ 目录进阶用法
- 测试类型对比表:
测试类型 测试范围 执行速度 适用场景 单元测试 单个函数/类 快 核心逻辑验证 端到端测试 完整API流程 慢 接口契约验证 - 测试覆盖率配置:
在
jest.config.js
中添加:module.exports = { collectCoverage: true, coverageDirectory: "coverage", }
javascript
配置文件深度解析
nest-cli.json 关键配置项
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger/plugin"], // 支持Swagger文档生成
"assets": ["**/*.graphql"] // 非TS文件处理
}
}
json
package.json 脚本增强
- 常用开发脚本扩展:
"scripts": { "build": "nest build", // 生产环境构建 "format": "prettier --write \"src/**/*.ts\"", // 格式化代码 "lint": "eslint \"src/**/*.ts\" --fix", // 代码规范检查 "start:prod": "node dist/main" // 生产环境启动 }
json
TypeScript配置差异
- tsconfig.build.json(构建专用):
{ "exclude": ["node_modules", "test"], "compilerOptions": { "outDir": "./dist", "declaration": true // 生成类型声明文件 } }
json - tsconfig.json(开发专用):
{ "compilerOptions": { "experimentalDecorators": true, // 启用装饰器语法 "strictNullChecks": true // 严格空值检查 } }
json
开发工具配置进阶
VS Code插件深度集成
- Prettier配置示例(.prettierrc):
{ "semi": false, "singleQuote": true, "printWidth": 100 }
json - ESLint配置技巧:
- 安装类型检查插件:
npm install @typescript-eslint/eslint-plugin --save-dev
bash - 推荐规则配置:
{ "rules": { "@typescript-eslint/explicit-function-return-type": "error", "no-console": "warn" } }
json
- 安装类型检查插件:
License管理实战
- MIT License核心条款:
1. 允许商用、修改、私用 2. 要求保留原始许可声明 3. 不承担代码使用风险
markdown - 多协议选择场景:
- Apache 2.0:专利授权更明确
- BSD 3-Clause:限制更少
- AGPL:云服务场景需谨慎
扩展知识:现代化项目结构趋势
💡提示:对于大型项目,推荐采用DDD(领域驱动设计)目录结构,将代码按业务域而非技术层级组织。
常见问题解答
Q1:如何清理node_modules节省空间?
A1:使用npm prune
或npx rimraf node_modules
Q2:测试文件应该和源码放一起还是分开?
A2:NestJS推荐分离方案,但也可采用相邻方案:
src/
├── user/
│ ├── user.controller.ts
│ ├── user.controller.spec.ts # 相邻测试文件
text
Q3:如何统一团队编码风格?
A3:推荐方案:
- 提交时自动格式化(husky + lint-staged)
- 共享编辑器配置(.vscode/settings.json)
- CI流程中加入lint检查
延伸学习资源
项目启动与调试深度解析
package.json脚本全面解析
核心启动脚本
"scripts": {
"start": "nest start", // 生产模式启动(不带热重载)
"start:dev": "nest start --watch", // 开发模式(文件监听+热更新)
"start:debug": "nest start --debug --watch", // 调试模式(支持断点调试)
"start:prod": "node dist/main", // 生产环境运行编译后代码
"start:docker": "nest start --watch --preserveWatchOutput" // Docker开发模式
}
json
高级脚本配置
{
"start:profile": "NODE_ENV=production nest start --watch", // 性能分析模式
"start:test": "NODE_ENV=test nest start", // 测试环境启动
"start:multi": "nest start --watch --config=config/development.json" // 多环境配置
}
json
启动流程详细说明
1. 开发模式启动(推荐)
npm run start:dev
bash
底层实现流程:
关键特性:
- 使用
ts-node
实时编译TypeScript - 文件变更时智能增量编译(仅重新编译修改文件)
- 保持HTTP连接不中断的热更新机制
2. 调试模式实战
npm run start:debug
bash
VSCode调试配置(.vscode/launch.json):
{
"type": "node",
"request": "launch",
"name": "Debug NestJS",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug"],
"port": 9229,
"skipFiles": ["<node_internals>/**"]
}
json
调试技巧:
- 在控制器方法中添加
debugger
语句 - 使用VSCode的"调用堆栈"面板追踪请求流程
- 条件断点:右键断点设置表达式(如
req.method === 'POST'
)
3. 生产环境注意事项
npm run build && npm run start:prod
bash
优化建议:
- 使用
pm2
进程管理:pm2 start dist/main.js --name "nestjs-app" -i max
bash - 添加环境变量配置:
NODE_ENV=production PORT=8080 npm run start:prod
bash
路由映射高级解读
典型控制台输出:
[Nest] 29384 - 2023/08/20 14:30:45 LOG [RoutesResolver] AppController {/}:
[Nest] 29384 - 2023/08/20 14:30:45 LOG [RouterExplorer] Mapped {/, GET} route
[Nest] 29384 - 2023/08/20 14:30:45 LOG [NestApplication] Nest application successfully started
text
关键信息说明:
RoutesResolver
:显示控制器作用域(@Controller('users')
会显示UsersController {/users}
)RouterExplorer
:具体路由映射信息(包含HTTP方法和路径)- 启动耗时:最后一行显示应用初始化时间(可用于性能基准测试)
访问验证进阶技巧
1. 使用cURL测试
curl -X GET http://localhost:3000 \
-H "Accept: application/json" \
-v # 显示详细请求信息
bash
2. 自动化测试脚本
// test/http-test.js
const assert = require('assert');
const http = require('http');
http.get('http://localhost:3000', (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
assert.equal(data, 'Hello World!');
console.log('✅ 测试通过');
});
});
javascript
3. 压力测试(使用autocannon)
npx autocannon -c 100 -d 10 http://localhost:3000
bash
常见问题解决方案
Q1:端口冲突怎么办?
# 指定端口启动
npm run start:dev -- --port 4000
# 或在main.ts中动态获取
const PORT = process.env.PORT || 3000;
bash
Q2:热更新失效?
- 检查
nest-cli.json
:{ "sourceRoot": "src", "compilerOptions": { "webpack": true // 确保启用webpack热更新 } }
json
Q3:如何查看完整路由列表?
- 安装路由调试模块:
npm install @nestjs/core
bash - 在main.ts中添加:
const app = await NestFactory.create(AppModule, { logger: ['debug'] });
typescript
性能优化技巧
- 启动加速:
# 使用SWC编译器(替代TypeScript) npm install @nestjs/cli @swc/core -D
bash
修改nest-cli.json
:{ "compilerOptions": { "builder": "swc" } }
json - 内存优化:
# 启动时限制内存 node --max-old-space-size=512 dist/main.js
bash - 集群模式:
// main.ts if (cluster.isPrimary) { cluster.fork(); } else { await bootstrap(); }
typescript
延伸学习资源
核心文件深度解析与扩展
main.ts - 应用入口(增强版)
基础功能扩展
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // 新增验证管道
import { LoggingInterceptor } from './interceptors/logging.interceptor'; // 自定义拦截器
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局中间件配置
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动过滤非DTO字段
transform: true, // 自动类型转换
})
);
app.useGlobalInterceptors(new LoggingInterceptor()); // 全局日志拦截器
// 跨域配置
app.enableCors({
origin: ['http://localhost:4200'],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
});
// 端口动态配置
const PORT = process.env.PORT || 3000;
await app.listen(PORT, () => {
console.log(`🚀 服务已启动: http://localhost:${PORT}`);
});
}
bootstrap();
typescript
高级配置项
- 多环境监听:
await app.listen(3000, '0.0.0.0'); // 监听所有网络接口
typescript - 启动前钩子:
app.startAllMicroservices(); // 微服务专用 await app.init(); // 执行初始化模块
typescript - 终止信号处理:
process.on('SIGTERM', () => app.close());
typescript
app.module.ts - 根模块(企业级配置)
模块元数据增强
import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ConfigModule.forRoot(), // 环境变量
CacheModule.register(), // 缓存
ScheduleModule.forRoot(), // 定时任务
ThrottlerModule.forRoot({ // 限流
ttl: 60,
limit: 100,
}),
],
controllers: [AppController],
providers: [
AppService,
{
provide: 'CONNECTION', // 自定义Provider
useFactory: () => new DatabaseConnection(),
},
],
exports: [AppService], // 暴露服务给其他模块
})
export class AppModule {}
typescript
动态模块示例
static forRoot(config: ConfigOptions): DynamicModule {
return {
module: AppModule,
providers: [
{
provide: 'CONFIG',
useValue: config,
},
],
};
}
typescript
app.controller.ts - 路由控制器(RESTful增强)
完整路由装饰器
import {
Controller,
Get,
Post,
Body,
Param,
Query,
Headers,
HttpCode,
Header,
} from '@nestjs/common';
@Controller('api') // 基础路径
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello/:name')
@HttpCode(200)
@Header('Cache-Control', 'none')
getHello(
@Param('name') name: string,
@Query('debug') debug: boolean,
@Headers('authorization') auth: string
): string {
return this.appService.getCustomHello(name);
}
@Post('messages')
createMessage(@Body() messageDto: MessageDto) {
return this.appService.create(messageDto);
}
}
typescript
装饰器功能对照表
装饰器 | 作用 | 示例 |
---|---|---|
@Param() | 获取路径参数 | /users/:id |
@Query() | 获取查询参数 | ?page=1 |
@Body() | 获取请求体 | POST/PUT请求体 |
@Headers() | 获取请求头 | Authorization 头 |
@HttpCode() | 设置响应状态码 | @HttpCode(204) |
@Header() | 设置响应头 | @Header('ETag', '123') |
app.service.ts - 业务服务(高级模式)
服务层扩展实现
import { Injectable, Scope } from '@nestjs/common';
import { RedisService } from '@liaoliaots/nestjs-redis';
@Injectable({ scope: Scope.REQUEST }) // 请求级作用域
export class AppService {
constructor(
private readonly redisService: RedisService,
@Inject('CONNECTION') private connection: DatabaseConnection,
) {}
private cache = new Map<string, string>();
async getHello(): Promise<string> {
// Redis缓存示例
const cached = await this.redisService.getClient().get('hello');
if (cached) return cached;
const result = 'Hello World!';
await this.redisService.getClient().set('hello', result, 'EX', 60);
return result;
}
// 事务处理示例
async transactionalOperation() {
return this.connection.transaction(async (manager) => {
await manager.save(User, { name: 'Test' });
return manager.findOne(User, { where: { name: 'Test' } });
});
}
}
typescript
服务作用域对比
作用域类型 | 生命周期 | 适用场景 |
---|---|---|
DEFAULT | 单例(应用生命周期) | 无状态服务 |
REQUEST | 每个请求创建新实例 | 请求敏感数据(如用户上下文) |
TRANSIENT | 每次注入创建新实例 | 避免状态共享的服务 |
依赖注入系统深度解析
DI容器工作原理
自定义Provider模式
- 值Provider:
{ provide: 'API_KEY', useValue: process.env.API_KEY }
typescript - 类Provider:
{ provide: LoggerService, useClass: ProductionLogger }
typescript - 工厂Provider:
{ provide: 'DATA_SOURCE', useFactory: async () => { return await createConnection(); }, }
typescript
常见问题解决方案
Q1:循环依赖怎么处理?
// moduleA.ts
@Module({
imports: [forwardRef(() => ModuleB)],
})
export class ModuleA {}
// moduleB.ts
@Module({
imports: [forwardRef(() => ModuleA)],
})
export class ModuleB {}
typescript
Q2:如何全局捕获异常?
// main.ts
app.useGlobalFilters(new HttpExceptionFilter());
typescript
Q3:服务测试如何模拟依赖?
// app.service.spec.ts
const mockRedisService = {
getClient: jest.fn().mockReturnValue({
get: jest.fn().mockResolvedValue('cached'),
}),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AppService,
{ provide: RedisService, useValue: mockRedisService },
],
}).compile();
});
typescript
性能优化技巧
- 作用域选择:
- 优先使用
DEFAULT
作用域减少内存开销 - 必要时使用
TRANSIENT
避免状态污染
- 优先使用
- 懒加载模块:
@Module({ imports: [LazyModule.forRoot({ lazy: true })], })
typescript - 依赖树扁平化:
- 使用
@Global()
减少模块重复导入 - 合理设计模块边界
- 使用
延伸学习资源
NestJS架构思想深度解析
模块化设计(企业级实践)
模块关系拓扑图(进阶版)
模块类型详解
模块类型 | 特征 | 典型示例 |
---|---|---|
根模块 | 应用入口,导入所有功能模块 | AppModule |
功能模块 | 按业务域划分的独立单元 | UsersModule , OrdersModule |
共享模块 | 被多个模块复用的公共服务 | CommonModule , UtilsModule |
核心模块 | 全局基础配置和单例服务 | CoreModule |
延迟加载模块 | 按需动态加载的模块 | LazyFeatureModule |
动态模块配置示例
@Module({})
export class ConfigModule {
static forRoot(config: ConfigOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: config,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
typescript
分层架构演进路线
经典三层架构
清洁架构变体
各层职责细化
- 表现层(Presentation):
- 路由处理 (
Controllers
) - 参数校验 (
Pipes
) - 响应格式化 (
Interceptors
)
- 路由处理 (
- 应用层(Application):
- 业务流程编排 (
Services
) - 事务管理 (
Transaction
) - 权限控制 (
Guards
)
- 业务流程编排 (
- 领域层(Domain):
- 业务实体定义 (
Entities
) - 领域服务 (
Domain Services
) - 业务规则 (
Value Objects
)
- 业务实体定义 (
- 基础设施层(Infrastructure):
- 数据库访问 (
TypeORM/Prisma
) - 外部API调用 (
HttpService
) - 消息队列 (
RabbitMQ
)
- 数据库访问 (
依赖注入系统深度优化
依赖关系图谱
注入策略对比
注入方式 | 优点 | 缺点 |
---|---|---|
构造函数注入 | 强依赖,编译时检查 | 依赖过多时构造函数臃肿 |
属性注入 | 可选依赖,代码简洁 | 运行时才能发现依赖缺失 |
方法注入 | 动态配置依赖 | 破坏封装性 |
企业级架构最佳实践
模块设计原则
- 高内聚低耦合:
- 每个模块只关注一个业务域
- 模块间通过接口通信
- 依赖方向控制:
- 测试金字塔应用:
性能关键点
- 模块懒加载:
@Module({ imports: [LazyModule.forRoot({ lazy: true })] })
typescript - 作用域控制:
- 默认单例减少实例化开销
- 必要时使用请求级作用域
- 依赖树优化:
- 避免深层嵌套依赖
- 使用`@Global()``减少重复导入
常见架构问题解决方案
Q1:如何解决循环依赖?
// moduleA.ts
@Module({
imports: [forwardRef(() => ModuleB)],
})
export class ModuleA {}
// serviceA.ts
constructor(@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB) {}
typescript
Q2:微服务架构如何分层?
Q3:如何实现插件化架构?
// 动态加载插件
const plugin = await import('./plugins/' + pluginName);
app.register(plugin.default);
typescript
前沿架构趋势
- Serverless适配:
// lambda.handler.ts export const handler = async (event) => { const app = await NestFactory.create(AppModule); return app.resolve(Handler).process(event); };
typescript - GraphQL混合架构:
@Query(() => [User]) async users() { return this.userService.findAll(); }
typescript - CQRS模式实现:
延伸学习资源
测试框架集成深度解析
测试体系全景图
测试配置详解
目录结构规范
/test
├── e2e/ # 端到端测试
│ ├── app.e2e-spec.ts # 主应用测试
│ └── user.e2e-spec.ts # 业务模块测试
├── unit/ # 单元测试
│ ├── controllers/
│ ├── services/
│ └── pipes/
└── jest.config.js # 测试配置文件
text
Jest配置增强(jest.config.js)
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageThreshold: { // 覆盖率阈值
global: {
branches: 80,
functions: 90,
lines: 85,
statements: 85
}
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
setupFilesAfterEnv: ['./test/setup.ts'] // 测试初始化脚本
}
javascript
单元测试进阶实践
控制器测试模板
import { Test } from '@nestjs/testing';
import { AppController } from '../../src/app.controller';
import { AppService } from '../../src/app.service';
describe('AppController', () => {
let appController: AppController;
const mockAppService = {
getHello: jest.fn().mockReturnValue('Mocked Hello World!')
};
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [AppController],
providers: [
{ provide: AppService, useValue: mockAppService }
],
}).compile();
appController = moduleRef.get<AppController>(AppController);
});
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Mocked Hello World!');
expect(mockAppService.getHello).toHaveBeenCalled();
});
});
typescript
服务测试要点
- Mock数据库访问:
const mockRepository = {
findOne: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
};
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
UserService,
{ provide: getRepositoryToken(User), useValue: mockRepository }
],
}).compile();
});
typescript
- 异步操作测试:
it('should throw NotFoundException', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(userService.getUser(999)).rejects.toThrow(NotFoundException);
});
typescript
端到端测试专业方案
测试脚手架配置
// test/setup.ts
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from '../src/app.module';
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
typescript
API测试示例
import * as request from 'supertest';
describe('AppController (e2e)', () => {
it('/GET hello', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
it('/POST users', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice' })
.expect(201)
.then(response => {
expect(response.body.id).toBeDefined();
});
});
});
typescript
测试覆盖率优化
生成HTML报告
npm test -- --coverage --coverageReporters=html
bash
生成的报告位于/coverage/index.html
,包含:
- 行覆盖率
- 分支覆盖率
- 函数覆盖率
- 未覆盖代码定位
持续集成配置
# .github/workflows/test.yml
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v1
yaml
高级测试技巧
- 测试数据库隔离:
// jest-mongodb.config.js
module.exports = {
mongodbMemoryServerOptions: {
instance: { dbName: 'jest' },
binary: { version: '4.4.1' }
}
};
typescript
- 请求上下文模拟:
const mockRequest = {
user: { id: 1 },
headers: { 'x-request-id': 'test123' }
} as Request;
typescript
- 性能基准测试:
it('response time < 100ms', async () => {
const start = Date.now();
await request(app.getHttpServer()).get('/');
expect(Date.now() - start).toBeLessThan(100);
});
typescript
常见问题解决方案
Q1:如何测试私有方法?
// 通过公有方法间接测试
// 或使用类型断言绕过检查
const privateMethod = (service as any).privateMethod;
typescript
Q2:测试时如何跳过认证?
// 重写Guard提供者
{ provide: AuthGuard, useValue: { canActivate: () => true } }
typescript
Q3:模拟文件上传测试?
request(app.getHttpServer())
.post('/upload')
.attach('file', './test/fixtures/test.jpg')
typescript
测试驱动开发(TDD)流程
延伸学习资源
↑